 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui.console;

 import java.util.HashMap ;

 import org.eclipse.core.runtime.jobs.ISchedulingRule;
 import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.jface.resource.JFaceResources;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.BadPositionCategoryException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.Position;
 import org.eclipse.jface.text.Region;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.ui.internal.console.ConsoleDocument;
 import org.eclipse.ui.internal.console.ConsoleHyperlinkPosition;
 import org.eclipse.ui.internal.console.ConsolePatternMatcher;
 import org.eclipse.ui.part.IPageBookViewPage;

 /**
  * An abstract text console that supports regular expression matching and
  * hyperlinks.
  * <p>
  * Pattern match listeners can be registered with a console programmatically
  * or via the <code>org.eclipse.ui.console.consolePatternMatchListeners</code>
  * extension point.
  * </p>
  * <p>
  * Clients may subclass this class. Subclasses must provide a document partitioner.
  * </p>
  * @since 3.1
  */
 public abstract class TextConsole extends AbstractConsole {

     /**
      * The current width of the console. Used for fixed width consoles.
      * A value of <=0 means does not have a fixed width.
      */
     private int fConsoleWidth;
     /**
      * The current tab width
      */
     private int fTabWidth;
     /**
      * The font used by this console
      */
     private Font fFont;
     
     /**
      * The background color used by this console or <code>null</code> if default
      */
     private Color fBackground;
     
     /**
      * The Console's regular expression pattern matcher
      */
     private ConsolePatternMatcher fPatternMatcher;
     
     /**
      * The Console's document
      */
     private ConsoleDocument fDocument;
     
    /**
     * indication that the console's partitioner is not expecting more input
     */
     private boolean fPartitionerFinished = false;
     
     /**
      * Indication that the console's pattern matcher has finished.
      * (all matches have been found and all listeners notified)
      */
     private boolean fMatcherFinished = false;
     
     /**
      * indication that the console output complete property has been fired
      */
     private boolean fCompleteFired = false;

     
     /**
      * Map of client defined attributes
      */
     private HashMap fAttributes = new HashMap ();
     
     private IConsoleManager fConsoleManager = ConsolePlugin.getDefault().getConsoleManager();
     
    
     /* (non-Javadoc)
      * @see org.eclipse.ui.console.AbstractConsole#dispose()
      */
     protected void dispose() {
         super.dispose();
         fFont = null;
         synchronized(fAttributes) {
             fAttributes.clear();
         }
     }
     /**
      * Constructs a console with the given name, image descriptor, and lifecycle
      *
      * @param name name to display for this console
      * @param consoleType console type identifier or <code>null</code>
      * @param imageDescriptor image to display for this console or <code>null</code>
      * @param autoLifecycle whether lifecycle methods should be called automatically
      * when this console is added/removed from the console manager
      */
     public TextConsole(String name, String consoleType, ImageDescriptor imageDescriptor, boolean autoLifecycle) {
         super(name, consoleType, imageDescriptor, autoLifecycle);
         fDocument = new ConsoleDocument();
         fDocument.addPositionCategory(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
         fPatternMatcher = new ConsolePatternMatcher(this);
         fDocument.addDocumentListener(fPatternMatcher);
         fTabWidth = IConsoleConstants.DEFAULT_TAB_SIZE;
     }

     /* (non-Javadoc)
      * @see org.eclipse.ui.console.IConsole#createPage(org.eclipse.ui.console.IConsoleView)
      */
     public IPageBookViewPage createPage(IConsoleView view) {
         return new TextConsolePage(this, view);
     }
     
     /**
      * Returns this console's document.
      * <p>
      * Note that a console may or may not support direct manipulation of its document.
      * For example, an I/O console document and its partitions are produced from the
      * streams connected to it, and clients are not intended to modify the document's
      * contents.
      * </p>
      *
      * @return this console's document
      */
     public IDocument getDocument() {
         return fDocument;
     }
     
     /**
      * Returns the current width of this console. A value of zero of less
      * indicates this console has no fixed width.
      *
      * @return the current width of this console
      */
     public int getConsoleWidth() {
         return fConsoleWidth;
     }
     
     /**
      * Sets the width of this console in characters. Any value greater than zero
      * will cause this console to have a fixed width.
      *
      * @param width the width to make this console. Values of 0 or less imply
      * the console does not have any fixed width.
      */
     public void setConsoleWidth(int width) {
         if (fConsoleWidth != width) {
             int old = fConsoleWidth;
             fConsoleWidth = width;
             
             firePropertyChange(this, IConsoleConstants.P_CONSOLE_WIDTH, new Integer (old), new Integer (fConsoleWidth));
         }
     }

     /**
      * Sets the tab width used in this console.
      *
      * @param newTabWidth the tab width
      */
     public void setTabWidth(final int newTabWidth) {
         if (fTabWidth != newTabWidth) {
             final int oldTabWidth = fTabWidth;
             fTabWidth = newTabWidth;
             ConsolePlugin.getStandardDisplay().asyncExec(new Runnable () {
                 public void run() {
                     firePropertyChange(TextConsole.this, IConsoleConstants.P_TAB_SIZE, new Integer (oldTabWidth), new Integer (fTabWidth));
                 }
             });
         }
     }
     
     /**
      * Returns the tab width used in this console.
      *
      * @return tab width used in this console
      */
     public int getTabWidth() {
         return fTabWidth;
     }
     
     /**
      * Returns the font used by this console. Must be called in the UI thread.
      *
      * @return font used by this console
      */
     public Font getFont() {
         if (fFont == null) {
             fFont = getDefaultFont();
         }
         return fFont;
     }
     
     /**
      * Returns the default text font.
      *
      * @return the default text font
      */
     private Font getDefaultFont() {
         return JFaceResources.getFont(JFaceResources.TEXT_FONT);
     }
     
     /**
      * Sets the font used by this console. Specify <code>null</code> to use
      * the default text font.
      *
      * @param newFont font, or <code>null</code> to indicate the default font
      */
     public void setFont(Font newFont) {
         // ensure font is initialized
 getFont();
         // translate null to default font
 if (newFont == null) {
             newFont = getDefaultFont();
         }
         // fire property change if required
 if (!fFont.equals(newFont)) {
             Font old = fFont;
             fFont = newFont;
             firePropertyChange(this, IConsoleConstants.P_FONT, old, fFont);
         }
     }
     
     /**
      * Sets the background color used by this console. Specify <code>null</code> to use
      * the default background color.
      *
      * @param background background color or <code>null</code> for default
      * @since 3.3
      * @deprecated use setBackground(Color) instead
      */
     public void setBackgrond(Color background) {
         setBackground(background);
     }
     
     /**
      * Sets the background color used by this console. Specify <code>null</code> to use
      * the default background color.
      *
      * @param background background color or <code>null</code> for default
      * @since 3.3
      */
     public void setBackground(Color background) {
         if (fBackground == null) {
             if (background == null) {
                 return;
             }
         } else if (fBackground.equals(background)){
             return;
         }
         Color old = fBackground;
         fBackground = background;
         firePropertyChange(this, IConsoleConstants.P_BACKGROUND_COLOR, old, fBackground);
     }
     
     /**
      * Returns the background color to use for this console or <code>null</code> for the
      * default background color.
      *
      * @return background color or <code>null</code> for default
      * @since 3.3
      */
     public Color getBackground() {
         return fBackground;
     }
     
     /**
      * Clears the console.
      * <p>
      * Since a console may or may not support direct manipulation
      * of its document's contents, this method should be called to clear a text console's
      * document. The default implementation sets this console's document content
      * to the empty string directly. Subclasses should override as required.
      * </p>
      */
     public void clearConsole() {
         IDocument document = getDocument();
         if (document != null) {
             document.set(""); //$NON-NLS-1$
 }
     }

     /**
      * Returns the console's document partitioner.
      * @return The console's document partitioner
      */
     protected abstract IConsoleDocumentPartitioner getPartitioner();
     
     /**
      * Returns all hyperlinks in this console.
      *
      * @return all hyperlinks in this console
      */
     public IHyperlink[] getHyperlinks() {
         try {
             Position[] positions = getDocument().getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
             IHyperlink[] hyperlinks = new IHyperlink[positions.length];
             for (int i = 0; i < positions.length; i++) {
                 ConsoleHyperlinkPosition position = (ConsoleHyperlinkPosition) positions[i];
                 hyperlinks[i] = position.getHyperLink();
             }
             return hyperlinks;
         } catch (BadPositionCategoryException e) {
             return new IHyperlink[0];
         }
     }
     
     /**
      * Returns the hyperlink at the given offset of <code>null</code> if none.
      *
      * @param offset offset for which a hyperlink is requested
      * @return the hyperlink at the given offset of <code>null</code> if none
      */
     public IHyperlink getHyperlink(int offset) {
         try {
             IDocument document = getDocument();
             if (document != null) {
                 Position[] positions = document.getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
                 Position position = findPosition(offset, positions);
                 if (position instanceof ConsoleHyperlinkPosition) {
                     return ((ConsoleHyperlinkPosition) position).getHyperLink();
                 }
             }
         } catch (BadPositionCategoryException e) {
         }
         return null;
     }

     /**
      * Binary search for the position at a given offset.
      *
      * @param offset the offset whose position should be found
      * @return the position containing the offset, or <code>null</code>
      */
     private Position findPosition(int offset, Position[] positions) {
         
         if (positions.length == 0)
             return null;
             
         int left= 0;
         int right= positions.length -1;
         int mid= 0;
         Position position= null;
         
         while (left < right) {
             
             mid= (left + right) / 2;
                 
             position= positions[mid];
             if (offset < position.getOffset()) {
                 if (left == mid)
                     right= left;
                 else
                     right= mid -1;
             } else if (offset > (position.getOffset() + position.getLength() - 1)) {
                 if (right == mid)
                     left= right;
                 else
                     left= mid +1;
             } else {
                 left= right= mid;
             }
         }
         
         position= positions[left];
         if (offset >= position.getOffset() && (offset < (position.getOffset() + position.getLength()))) {
             return position;
         }
         return null;
     }

     /**
      * Adds the given pattern match listener to this console. The listener will
      * be connected and receive match notifications. Has no effect if an identical
      * listener has already been added.
      *
      * @param listener the listener to add
      */
     public void addPatternMatchListener(IPatternMatchListener listener) {
         fPatternMatcher.addPatternMatchListener(listener);
     }
     
     /**
      * Removes the given pattern match listener from this console. The listener will be
      * disconnected and will no longer receive match notifications. Has no effect
      * if the listener was not previously added.
      *
      * @param listener the pattern match listener to remove
      */
     public void removePatternMatchListener(IPatternMatchListener listener) {
         fPatternMatcher.removePatternMatchListener(listener);
     }
     
     
     /**
      * Job scheduling rule that prevent the job from running if the console's PatternMatcher
      * is active.
      */
     private class MatcherSchedulingRule implements ISchedulingRule {
         public boolean contains(ISchedulingRule rule) {
             return rule == this;
         }

         public boolean isConflicting(ISchedulingRule rule) {
             if (contains(rule)) {
                 return true;
             }
             if (rule != this && rule instanceof MatcherSchedulingRule) {
                 return (((MatcherSchedulingRule)rule).getConsole() == TextConsole.this);
             }
             return false;
         }
         
         public TextConsole getConsole() {
             return TextConsole.this;
         }
     }
     
     /**
      * Returns a scheduling rule which can be used to prevent jobs from running
      * while this console's pattern matcher is active.
      * <p>
      * Although this scheduling rule prevents jobs from running at the same time as
      * pattern matching jobs for this console, it does not enforce any ordering of jobs.
      * Since 3.2, pattern matching jobs belong to the job family identified by the console
      * object that matching is occurring on. To ensure a job runs after all scheduled pattern
      * matching is complete, clients must join on this console's job family.
      * </p>
      * @return a scheduling rule which can be used to prevent jobs from running
      * while this console's pattern matcher is active
      */
     public ISchedulingRule getSchedulingRule() {
         return new MatcherSchedulingRule();
     }
     
     /**
      * This console's partitioner should call this method when it is not expecting any new data
      * to be appended to the document.
      */
     public void partitionerFinished() {
         fPatternMatcher.forceFinalMatching();
         fPartitionerFinished = true;
         checkFinished();
     }
     
     /**
      * Called by this console's pattern matcher when matching is complete.
      * <p>
      * Clients should not call this method.
      * <p>
      */
     public void matcherFinished() {
         fMatcherFinished = true;
         fDocument.removeDocumentListener(fPatternMatcher);
         checkFinished();
     }
     
     /**
      * Fires the console output complete property change event.
      */
     private synchronized void checkFinished() {
         if (!fCompleteFired && fPartitionerFinished && fMatcherFinished ) {
             fCompleteFired = true;
             firePropertyChange(this, IConsoleConstants.P_CONSOLE_OUTPUT_COMPLETE, null, null);
         }
     }
     
     /**
      * Adds a hyperlink to this console.
      *
      * @param hyperlink the hyperlink to add
      * @param offset the offset in the console document at which the hyperlink should be added
      * @param length the length of the text which should be hyperlinked
      * @throws BadLocationException if the specified location is not valid.
      */
     public void addHyperlink(IHyperlink hyperlink, int offset, int length) throws BadLocationException {
         IDocument document = getDocument();
         ConsoleHyperlinkPosition hyperlinkPosition = new ConsoleHyperlinkPosition(hyperlink, offset, length);
         try {
             document.addPosition(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY, hyperlinkPosition);
             fConsoleManager.refresh(this);
         } catch (BadPositionCategoryException e) {
             ConsolePlugin.log(e);
         }
     }
     
     /**
      * Returns the region associated with the given hyperlink.
      *
      * @param link hyperlink
      * @return the region associated with the hyperlink or null if the hyperlink is not found.
      */
     public IRegion getRegion(IHyperlink link) {
         try {
             IDocument doc = getDocument();
             if (doc != null) {
                 Position[] positions = doc.getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
                 for (int i = 0; i < positions.length; i++) {
                     ConsoleHyperlinkPosition position = (ConsoleHyperlinkPosition)positions[i];
                     if (position.getHyperLink().equals(link)) {
                         return new Region(position.getOffset(), position.getLength());
                     }
                 }
             }
         } catch (BadPositionCategoryException e) {
         }
         return null;
     }
     
     /**
      * Returns the attribute associated with the specified key.
      *
      * @param key attribute key
      * @return the attribute associated with the specified key
      */
     public Object getAttribute(String key) {
         synchronized (fAttributes) {
             return fAttributes.get(key);
         }
     }
     
     /**
      * Sets an attribute value. Intended for client data.
      *
      * @param key attribute key
      * @param value attribute value
      */
     public void setAttribute(String key, Object value) {
         synchronized(fAttributes) {
             fAttributes.put(key, value);
         }
     }
 }

